import ij.IJ;
import ij.ImagePlus;
import ij.process.ImageProcessor;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Timer;

/**
*
* Bachelor thesis at the Institute of Biomedical Engineering, 
* located at the UMIT/EWZ campus in Hall in Tyrol. <br><br>
* 
* This class offers support for morphological Operations after the sgmentation of
* bloodmasses. 
* 
* @author Stefan Fuchs
*
*/
public class AlternatingSequentialFilter implements Runnable, ActionListener, WindowListener{
	
	//int[]
	/** The ConnectedComponentLabel from the segmentation*/
	private int[] connectedComponentLabel = null;
	
	/** Label for morphological operations*/
	private int[] label = null;
	
	/** Label for morphological operations*/
	private int[] labelT1 = null;
	
	/** Final Label with the Result*/
	private int[] asfLabel = null;
	
	//int
	/** Height of the Image */
	private int height;

	/** Width of the Image */
	private int width;

	/** Amount of Slices in the stack */
	private int size;
	
	/** Min Roi-Prameters X-Axis */
	private int minRoiX;

	/** Max Roi-Prameters X-Axis */
	private int maxRoiX;

	/** Min Roi-Prameters Y-Axis */
	private int minRoiY;

	/** Max Roi-Prameters Y-Axis */
	private int maxRoiY;

	/** Min Roi-Prameters Z-Axis */
	private int minRoiZ;

	/** Max Roi-Prameters Z-Axis */
	private int maxRoiZ;

	/** Amount of structur elements*/
	private int structureElements;
	
	/** The amount of connected components*/
	private int maxConectedComponentLabelValue = 0;
	
	/** sequence counter for image names*/
	private int seqCounter = 0;

	//String
	/** Name of current image. */
	private String imageName;
	
	/** The full Image Title*/
	private String fullImageTitle = null;
	
	//boolean
	/** Indicator if segmentation is running or not. */
	private boolean running = false;
	
	/** Indicator wheter Open-Close or Close-Open ASF should be used*/
	private boolean openClose = false;
	
	//ArrayList
	/** Arralist of Voxels with final outcome to visualize*/
	ArrayList<Voxel> asfVoxels = null;
	
	//General Objects

	/** Timer to update time left. */
	private Timer timer;

	/** Segmentation thread. */
	private Thread morphThread;

	/** Status-Dialog to display segmentation progress */
	private StatusDialog sd;
	
	/** The modell*/
	private Modell modell;
	
	//////////////////////
	// Lets start here ///
	//////////////////////
	
	/** The singleton instance*/
	private static AlternatingSequentialFilter instance = null;

	/**
	 * Method to get the singleton instance of {@link AlternatingSequentialFilter}
	 * 
	 * @return
	 */
	public static synchronized AlternatingSequentialFilter getInstance() {
		
		if ( instance == null ) {
			instance = new AlternatingSequentialFilter();
		}
		return instance;
	}
	
	/**
	 * Constructor
	 */
	private AlternatingSequentialFilter() {
		//dummyConstructor
	}
	
	//////////////////////////////////////////////
	
	/**
	 * Prepear the morphological peration, create a new thread
	 */
	public void prepearAlternatingSequentialFilter(){
		this.running = true;
		this.morphThread = new Thread(AlternatingSequentialFilter.getInstance());
		this.morphThread.start();
	}

	@Override
	public void run() {
		
		long start = System.currentTimeMillis();
		this.modell = Modell.getInstance();
		getDimensionParameters();
		
		this.sd = new StatusDialog();
		this.sd.getBtnAbort().addActionListener(this);
		this.sd.getJfDialog().addWindowListener(this);

		prepearDataStructures();
				
		// This is where the magic happenes
		startAlternatingSequenceFilter();
		
		getAlternatingSequenceFilterResults();
		visualizeAlternatingSequenceFilter(" ASF");
		
		this.sd.close();
		
		this.modell.setAlternatingSequenceFilteringLabel(Util.copy1DVoxelArry(this.asfLabel));
		showGrayImage();
		this.running = false;
		
		long stop = System.currentTimeMillis();
		long time = stop - start;
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		System.out.println("ASF Duration: " + sdf.format(time));
		
		clearAll();
	}
	
	/**
	 * Displays the grayvalue image to find out treshholds
	 */
	private void showGrayImage() {
		String title = "8-bit Grayvalue Image for Treshold-values";
		
		if ( ij.WindowManager.getImage(title) == null ) {
			ImagePlus grayImage = new ImagePlus(title, this.modell.getGrayValueImageStack());
			grayImage.setSlice(1);
			grayImage.show();
		}
	}
	
	/**
	 * Prepares the image and initializes the data structures for further
	 * Morphological commands.
	 */
	private void prepearDataStructures() {
		
		this.connectedComponentLabel = this.modell.getConnectedComponentLabel();
				
		this.label = new int[this.width * this.height * this.size];
		this.labelT1 = new int[this.width * this.height * this.size];
		this.asfLabel = new int[this.width * this.height * this.size];
		
		int coordinate1D;
		
		//Copy the the original to the Label
		for (int z = 0; z < this.size ; z++) {
			for (int y = 0; y < this.height ; y++) {
				for (int x = 0 ; x < this.width ; x++) {
					
					coordinate1D = (int) (this.width * (this.height * z + y) + x);
					
					this.label[coordinate1D] = 0;
					this.labelT1[coordinate1D] = 0;
					this.asfLabel[coordinate1D] = 0;
					
					if (this.maxConectedComponentLabelValue < this.connectedComponentLabel[coordinate1D]) {
						this.maxConectedComponentLabelValue = this.connectedComponentLabel[coordinate1D];
					}
				}
			}
		}
	}
	
	/**
	 * Start the ASF function
	 */
	private void startAlternatingSequenceFilter() {
		this.sd.setTitle("Alternating Sequence Filtering");
		//this.sd.setTime("Time left: --:--:--");
		this.sd.setTime("");
		this.sd.show(true);
		
		this.timer = new Timer();
		
		for ( int labelValue = 0; labelValue < this.maxConectedComponentLabelValue ; labelValue++ ) {
			
			prepearLabel(labelValue + 1);
			
			for ( int strElements = 0 ; strElements < this.structureElements ; strElements++) {
				
				this.sd.setStatus("ASF done: " + (int)(100 * (labelValue + strElements) / (this.maxConectedComponentLabelValue + this.structureElements)) + " %");
				
				//Loop 1
				for ( int z = this.minRoiZ ; z < this.maxRoiZ ; z++ ){
					for ( int y = this.minRoiY ; y < this.maxRoiY ; y++ ) {
						for ( int x = this.minRoiX ; x < this.maxRoiX ; x++ ) {
							
							if (this.openClose) {
								//erode
								erode(x, y, z, strElements+1);
							} else {
								//dilate
								dilate(x, y, z, strElements+1);
							}
						}
					}
				}
				
				//labelT1 -> label
				prepearNextStep();
				
				//Loop 2
				for ( int z = this.minRoiZ ; z < this.maxRoiZ ; z++ ){
					for ( int y = this.minRoiY ; y < this.maxRoiY ; y++ ) {
						for ( int x = this.minRoiX ; x < this.maxRoiX ; x++ ) {
							
							if (this.openClose) {
								//delate
								dilate(x, y, z, strElements+1);
							} else {
								//erode
								erode(x, y, z, strElements+1);
							}
						}
					}
				}
				
				//labelT1 -> label
				prepearNextStep();
				
			}
			writeASFLabel(labelValue +1 );
		}
	}
	
	/**
	 * Write to correct labeld values into the ASF label
	 * 
	 * @param value
	 * 		label value if the voxel
	 */
	private void writeASFLabel(int value) {
		
		int coordinate1D;
		
		for ( int z = 0 ; z < this.size ; z++) {
			for ( int y = 0 ; y < this.height ; y++) {
				for ( int x = 0; x < this.width ; x++) {
					
					coordinate1D = (this.width * ( this.height * z + y) + x);
					
					if (this.label[coordinate1D] == 1) {
						this.asfLabel[coordinate1D] = value;
					}
					
					//Clear label
					this.label[coordinate1D] = 0;
				}
			}
		}
	}
	
	/**
	 * Write the values of one connected component into the label for ASF
	 * 
	 * @param value
	 * 		the value to be copied
	 */
	private void prepearLabel(int value) {
		
		int coordinate1D;
		
		for ( int z = 0 ; z < this.size ; z++) {
			for ( int y = 0 ; y < this.height ; y++) {
				for ( int x = 0; x < this.width ; x++) {
			
					coordinate1D = (this.width * ( this.height * z + y) + x);
					if (connectedComponentLabel[coordinate1D] == value) {
						this.label[coordinate1D] = 1;
					} else {
						this.label[coordinate1D] = 0;
					}
				}
			}
		}
	}
	
	
	/**
	 * Erode the image
	 * 
	 * @param x
	 * 		x-coordinate of the center
	 * @param y
	 * 		y-coordinate of the center
	 * @param z
	 * 		z-coordinate of the center
	 * @param labelValue
	 * 		the value of the foreground, other values are considered background
	 * @param element
	 * 		the structure element
	 */
	private void erode(int x, int y, int z, int element) {
		
		
		int center1D = (this.width * (this.height * z  + y) + x);
		int tmpCoord1D;
		
		for ( int yy = (y - element) ; yy <= (y + element) ; yy++) {
			for ( int xx = (x - element) ; xx <= (x + element) ; xx++) {
				
				//TODO: Check boundary value?!
				tmpCoord1D = (this.width * ( this.height * z +yy) + xx);
				
				if ( label[tmpCoord1D] != 1) {
					this.labelT1[center1D] = 0;
					return;
				}
			}
		}
		this.labelT1[center1D] = 1;
	}

	/**
	 * Dilate the image
	 * 
	 * @param x
	 * 		x-coordinate of the center
	 * @param y
	 * 		y-coordinate of the center
	 * @param z
	 * 		z-coordinate of the center
	 * @param labelValue
	 * 		the value of the foreground, other values are considered background
	 * @param element
	 * 		the structure element
	 */
	private void dilate(int x, int y, int z, int element) {
		
		int center1D = (this.width * (this.height * z  + y) + x);
		int tmpCoord1D;
		
		for ( int yy = (y - element) ; yy <= (y + element) ; yy++) {
			for ( int xx = (x - element) ; xx <= (x + element) ; xx++) {
				
				//TODO: Check boundary value?!
				tmpCoord1D = (this.width * ( this.height * z +yy) + xx);
				
				if ( label[tmpCoord1D] == 1) {
					this.labelT1[center1D] = 1;
					return;
				}
			}
		}
		this.labelT1[center1D] = 0;
	}
	
	/**
	 * labelT1 -> label
	 */
	private void prepearNextStep() {
		int coordinate1D;
		
		for (int z = 0; z < this.size ; z++) {
			for (int y = 0; y < this.height ; y++) {
				for (int x = 0 ; x < this.width ; x++) {
					
					coordinate1D = (int) (this.width * (this.height * z + y) + x);
					this.label[coordinate1D] = this.labelT1[coordinate1D];
					this.labelT1[coordinate1D] = 0;
				}
			}
		}
	}
	
	/**
	 * Get all labeled {@link Voxel}s into an {@link ArrayList}
	 */
	private void getAlternatingSequenceFilterResults() {
		
		this.sd.setTime("");
		int coordinate1D;
		this.asfVoxels = new ArrayList<Voxel>();
		
		for(int z = this.minRoiZ; z < this.maxRoiZ; z++){
			
			this.sd.setStatus("Calculating ASF: " + (int)(100 * (z - this.minRoiZ) / (this.maxRoiZ - this.minRoiZ)) + " %");
			
			for (int y = this.minRoiY; y < this.maxRoiY; y++){
				for (int x = this.minRoiX; x < this.maxRoiX; x++){
					
					coordinate1D = (int)(this.width * (this.height * z + y) + x);
					if (asfLabel[coordinate1D] == 1 ) {
						this.asfVoxels.add(new Voxel(x,y,( z + 1  ),Modell.VENTRICLE_A_POINTS_LABEL.getRGB()));
					}
					if (asfLabel[coordinate1D] == 2) {
						this.asfVoxels.add(new Voxel(x,y,( z + 1 ),Modell.VENTRICLE_B_POINTS_LABEL.getRGB()));
					}
				}
			}
		}
	}

	/**
	 * Visualize the result
	 * 
	 * @param title
	 * 			The title of the image
	 */
	private void visualizeAlternatingSequenceFilter(String title){
		
		this.sd.setStatus("Visualizing ASF Result...");
		String counter;
		
		/*
		 * Create postfix number
		 */
		if(this.seqCounter <= 9){
			counter = "00" + this.seqCounter;
		}
		else if(this.seqCounter > 9 && this.seqCounter <= 99){
			counter = "0" + this.seqCounter;
		}
		else{
			counter = "" + this.seqCounter;
		}
		
		//Close Image if its opened
		if (ij.WindowManager.getImage(this.imageName + title + "[" + counter + "]") != null ) {
			if (ij.WindowManager.getImage(this.imageName + title + "[" + counter + "]").isVisible() ) {
			ij.WindowManager.getImage(this.imageName + title + "[" + counter + "]").changes = false;
			ij.WindowManager.getImage(this.imageName + title + "[" + counter + "]").close();
			}
		}
		
		this.fullImageTitle = this.imageName + title + "[" + counter + "]";
				
		ImagePlus segmentedLabeledPointsImage = new ImagePlus(this.imageName + title, this.modell.getImageStack());
		ImageProcessor segmentedLabeledPointsImageProc = segmentedLabeledPointsImage.getProcessor();
		
		for (Voxel v : this.asfVoxels) {
			segmentedLabeledPointsImage.setSlice(v.getZ());
			segmentedLabeledPointsImageProc.putPixel(v.getX(), v.getY(), v.getValue());
		}
		
		segmentedLabeledPointsImage.setTitle(segmentedLabeledPointsImage.getTitle() + "[" + counter + "]");
		segmentedLabeledPointsImage.setSlice(1);
		segmentedLabeledPointsImage.show();
	}
	
	/**
	 * Clear unneeded components
	 */
	private void clearAll() {
		this.label = null;
		this.labelT1 = null;
		this.connectedComponentLabel = null;
		this.sd = null;
		this.asfVoxels = null;
		this.asfLabel = null;
		
		this.modell = null;
		
		IJ.freeMemory();
		System.gc();
	}
	
	/**
	 * Reads the dimension-Parameters from the model and sets them accordingly
	 */
	private void getDimensionParameters() {
		this.width = this.modell.getWidth();
		this.height = this.modell.getHeight();
		this.size = this.modell.getSize();
		this.minRoiX = this.modell.getMinRoiX();
		this.maxRoiX = this.modell.getMaxRoiX();
		this.minRoiY = this.modell.getMinRoiY();
		this.maxRoiY = this.modell.getMaxRoiY();
		this.minRoiZ = this.modell.getMinRoiZ();
		this.maxRoiZ = this.modell.getMaxRoiZ();
	}
	
	////////////////////////
	// Setters & Getters ///
	////////////////////////
	
	/**
	 * Set Image name
	 * 
	 * @param imageName
	 *            Image name
	 */
	public void setImageName(String imageName) {
		this.imageName = imageName;
	}
	
	/**
	 * Indicates if the Segmentation is still running
	 * 
	 * @return true - still running <br>
	 *         false - not running
	 */
	public boolean isRunning() {
		return running;
	}
	
	/**
	 * Set the amount of structure elements
	 * 
	 * @param structureElements
	 * 			Amount of elements			
	 */
	public void setStructureElements(int structureElements) {
		this.structureElements = structureElements;
	}
	
	/**
	 * Set the Operation Order
	 * 
	 * @param openClose
	 * 			true:  Open-Close
	 * 			false: Close-Open
	 */
	public void setOpenClose(boolean openClose) {
		this.openClose = openClose;
	}
	
	/**
	 * Set the sequence counter
	 * 
	 * @param seqCounter
	 * 			Sequence counter
	 */
	public void setSeqCounter(int seqCounter) {
		this.seqCounter = seqCounter;
	}
	
	/**
	 * Returns the full image title
	 * @return
	 */
	public String getFullImageTitle() {
		return fullImageTitle;
	}

	//////////////////////////////////////////////////////////
	
	//ActionListener
	@SuppressWarnings("deprecation")
	@Override
	public void actionPerformed(ActionEvent e) {
		if(e.getSource() == this.sd.getBtnAbort()){
			synchronized (this.morphThread){
				if(this.timer != null){
					this.timer.cancel();
					this.timer = null;
				}
				System.gc();
				this.morphThread.stop();
			}
			this.running = false;
			this.sd.close();	
		}
		
	}

	
	
	////////////////////////////////////////////////////////////
	
	//WindowListener
	@Override
	public void windowActivated(WindowEvent e) {
		//empty block
	}

	@Override
	public void windowClosed(WindowEvent e) {
		//empty block
	}

	
	@Override
	@SuppressWarnings("deprecation")
	public void windowClosing(WindowEvent e) {
		if(e.getSource() == this.sd.getBtnAbort()){
			synchronized (this.morphThread){
				if(this.timer != null){
					this.timer.cancel();
					this.timer = null;
				}
				System.gc();
				this.morphThread.stop();
			}
			this.running = false;
			this.sd.close();	
		}
	}

	@Override
	public void windowDeactivated(WindowEvent e) {
		//empty block
	}

	@Override
	public void windowDeiconified(WindowEvent e) {
		//empty block
	}

	@Override
	public void windowIconified(WindowEvent e) {
		//empty block
	}

	@Override
	public void windowOpened(WindowEvent e) {
		//empty block
	}
	
}
